use super::cobdirection::COBDirection;
use super::cobregister::{COBSelRegister, COBSwRegister};

#[allow(unused)]
pub struct COBUnitConnector {
    pub from_dir: COBDirection,
    pub to_dir: COBDirection,
    pub from_index: usize,
    pub to_index: usize,

    pub sw_reg: COBSwRegister,

    pub t_to_c_sel_reg: COBSelRegister,
    pub c_to_t_sel_reg: COBSelRegister,
}

impl COBUnitConnector {
    pub fn new(
        from_dir: COBDirection,
        from_index: usize,
        to_dir: COBDirection,
        to_index: usize,
        sw_reg: COBSwRegister,
        t_to_c_sel_reg: COBSelRegister,
        c_to_t_sel_reg: COBSelRegister,
    ) -> Self {
        Self {
            from_dir,
            to_dir,
            from_index,
            to_index,
            sw_reg,
            t_to_c_sel_reg,
            c_to_t_sel_reg,
        }
    }

    pub fn is_connected(&self) -> bool {
        self.sw_reg.get()
    }
}

pub const COBUNIT_WILTON_SIZE: usize = 8;

#[derive(Clone)]
pub struct COBUnit {
    switch_ru: Vec<COBSwRegister>,
    switch_lu: Vec<COBSwRegister>,
    switch_rd: Vec<COBSwRegister>,
    switch_ld: Vec<COBSwRegister>,

    switch_lr: Vec<COBSwRegister>,
    switch_ud: Vec<COBSwRegister>,

    left_sel: Vec<COBSelRegister>,
    right_sel: Vec<COBSelRegister>,
    up_sel: Vec<COBSelRegister>,
    down_sel: Vec<COBSelRegister>,
}

fn build_sel_regs() -> Vec<COBSelRegister> {
    (0..COBUNIT_WILTON_SIZE)
        .into_iter()
        .map(|_| COBSelRegister::new())
        .collect()
}

fn build_sw_regs() -> Vec<COBSwRegister> {
    (0..COBUNIT_WILTON_SIZE)
        .into_iter()
        .map(|_| COBSwRegister::new())
        .collect()
}

impl COBUnit {
    pub fn new() -> Self {
        Self {
            switch_ru: build_sw_regs(),
            switch_lu: build_sw_regs(),
            switch_rd: build_sw_regs(),
            switch_ld: build_sw_regs(),
            switch_lr: build_sw_regs(),
            switch_ud: build_sw_regs(),

            left_sel: build_sel_regs(),
            right_sel: build_sel_regs(),
            up_sel: build_sel_regs(),
            down_sel: build_sel_regs(),
        }
    }
}

impl COBUnit {
    pub fn adjacent_connectors_from_left(&self, left_index: usize) -> Vec<COBUnitConnector> {
        self.adjacent_connectors(COBDirection::Left, left_index)
    }

    pub fn adjacent_connectors_from_right(&self, right_index: usize) -> Vec<COBUnitConnector> {
        self.adjacent_connectors(COBDirection::Right, right_index)
    }

    pub fn adjacent_connectors_from_up(&self, up_index: usize) -> Vec<COBUnitConnector> {
        self.adjacent_connectors(COBDirection::Up, up_index)
    }

    pub fn adjacent_connectors_from_down(&self, down_index: usize) -> Vec<COBUnitConnector> {
        self.adjacent_connectors(COBDirection::Down, down_index)
    }

    pub fn adjacent_connectors(
        &self,
        from_dir: COBDirection,
        from_index: usize,
    ) -> Vec<COBUnitConnector> {
        COBUnit::assert_index(from_index);

        use COBDirection::*;
        [Left, Right, Down, Up]
            .into_iter()
            .filter(|&dir| dir != from_dir)
            .map(|to_dir| -> COBUnitConnector {
                self.get_connector(from_dir, from_index, to_dir)
            })
            .collect()
    }

    pub fn get_connector(
        &self,
        from_dir: COBDirection,
        from_index: usize,
        to_dir: COBDirection,
    ) -> COBUnitConnector {
        COBUnit::assert_index(from_index);

        let to_index = COBUnit::index_map(from_dir, from_index, to_dir);
        COBUnitConnector::new(
            from_dir,
            from_index,
            to_dir,
            to_index,
            self.sw_register(from_dir, from_index, to_dir),
            self.sel_register(from_dir, from_index),
            self.sel_register(to_dir, to_index),
        )
    }
}

impl COBUnit {
    pub fn sw_register(
        &self,
        from_dir: COBDirection,
        from_index: usize,
        to_dir: COBDirection,
    ) -> COBSwRegister {
        COBUnit::assert_index(from_index);

        let to_index = COBUnit::index_map(from_dir, from_index, to_dir);

        // Please make sure the arguments are match!
        use COBDirection::*;
        match from_dir {
            Left => match to_dir {
                Left => unreachable!(),
                Right => self.switch_lr[from_index].rc_clone(),
                Up => self.switch_lu[from_index].rc_clone(),
                Down => self.switch_ld[from_index].rc_clone(),
            },
            Right => match to_dir {
                Left => self.switch_lr[from_index].rc_clone(),
                Right => unreachable!(),
                Up => self.switch_ru[from_index].rc_clone(),
                Down => self.switch_rd[from_index].rc_clone(),
            },
            Up => match to_dir {
                Left => self.switch_lu[to_index].rc_clone(),
                Right => self.switch_ru[to_index].rc_clone(),
                Up => unreachable!(),
                Down => self.switch_ud[from_index].rc_clone(),
            },
            Down => match to_dir {
                Left => self.switch_ld[to_index].rc_clone(),
                Right => self.switch_rd[to_index].rc_clone(),
                Up => self.switch_ud[from_index].rc_clone(),
                Down => unreachable!(),
            },
        }
    }

    pub fn sel_register(&self, dir: COBDirection, index: usize) -> COBSelRegister {
        COBUnit::assert_index(index);
        use COBDirection::*;
        match dir {
            Left => self.left_sel[index].rc_clone(),
            Right => self.right_sel[index].rc_clone(),
            Up => self.up_sel[index].rc_clone(),
            Down => self.down_sel[index].rc_clone(),
        }
    }
}

impl COBUnit {
    pub fn index_map(from_dir: COBDirection, from_index: usize, to_dir: COBDirection) -> usize {
        COBUnit::assert_index(from_index);

        use COBDirection::*;
        match from_dir {
            Left => match to_dir {
                Left => unreachable!(),
                Right => COBUnit::left_map_right(from_index),
                Up => COBUnit::left_map_up(from_index),
                Down => COBUnit::left_map_down(from_index),
            },
            Right => match to_dir {
                Left => COBUnit::right_map_left(from_index),
                Right => unreachable!(),
                Up => COBUnit::right_map_up(from_index),
                Down => COBUnit::right_map_down(from_index),
            },
            Up => match to_dir {
                Left => COBUnit::up_map_left(from_index),
                Right => COBUnit::up_map_right(from_index),
                Up => unreachable!(),
                Down => COBUnit::up_map_down(from_index),
            },
            Down => match to_dir {
                Left => COBUnit::down_map_left(from_index),
                Right => COBUnit::down_map_right(from_index),
                Up => COBUnit::down_map_up(from_index),
                Down => unreachable!(),
            },
        }
    }

    fn left_map_up(index: usize) -> usize {
        COBUnit::assert_index(index);

        if index == COBUNIT_WILTON_SIZE - 1 {
            return COBUNIT_WILTON_SIZE - 1;
        } else {
            return COBUNIT_WILTON_SIZE - 2 - index;
        }
    }

    fn up_map_left(index: usize) -> usize {
        COBUnit::assert_index(index);

        if index == COBUNIT_WILTON_SIZE - 1 {
            return COBUNIT_WILTON_SIZE - 1;
        } else {
            return COBUNIT_WILTON_SIZE - 2 - index;
        }
    }

    fn up_map_right(index: usize) -> usize {
        COBUnit::assert_index(index);

        if index == COBUNIT_WILTON_SIZE - 1 {
            return 0;
        } else {
            return index + 1;
        }
    }

    fn right_map_up(index: usize) -> usize {
        COBUnit::assert_index(index);

        if index == 0 {
            return COBUNIT_WILTON_SIZE - 1;
        } else {
            return index - 1;
        }
    }

    fn right_map_down(index: usize) -> usize {
        COBUnit::assert_index(index);

        if index == COBUNIT_WILTON_SIZE - 1 {
            return COBUNIT_WILTON_SIZE - 1;
        } else {
            return COBUNIT_WILTON_SIZE - 2 - index;
        }
    }

    fn down_map_right(index: usize) -> usize {
        COBUnit::assert_index(index);

        if index == COBUNIT_WILTON_SIZE - 1 {
            return COBUNIT_WILTON_SIZE - 1;
        } else {
            return COBUNIT_WILTON_SIZE - 2 - index;
        }
    }

    fn down_map_left(index: usize) -> usize {
        COBUnit::assert_index(index);

        if index == 0 {
            return COBUNIT_WILTON_SIZE - 1;
        } else {
            return index - 1;
        }
    }

    fn left_map_down(index: usize) -> usize {
        COBUnit::assert_index(index);

        if index == COBUNIT_WILTON_SIZE - 1 {
            return 0;
        } else {
            return index + 1;
        }
    }

    fn left_map_right(index: usize) -> usize {
        return index;
    }

    fn right_map_left(index: usize) -> usize {
        return index;
    }

    fn up_map_down(index: usize) -> usize {
        return index;
    }

    fn down_map_up(index: usize) -> usize {
        return index;
    }

    fn assert_index(index: usize) {
        assert!(index < COBUNIT_WILTON_SIZE);
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_adjacent_connectors_1() {
        use COBDirection::*;

        let cobunit = COBUnit::new();
        let connectors = cobunit.adjacent_connectors_from_left(0);
        assert_eq!(connectors.len(), 3);
        for connector in &connectors {
            match connector.to_dir {
                Left => unreachable!(),
                Right => assert_eq!(connector.to_index, 0),
                Up => assert_eq!(connector.to_index, 6),
                Down => assert_eq!(connector.to_index, 1),
            }
        }
    }

    #[test]
    fn test_adjacent_connectors_2() {
        use COBDirection::*;

        let cobunit = COBUnit::new();
        let connectors = cobunit.adjacent_connectors_from_right(0);
        assert_eq!(connectors.len(), 3);
        for connector in &connectors {
            match connector.to_dir {
                Right => unreachable!(),
                Left => assert_eq!(connector.to_index, 0),
                Up => assert_eq!(connector.to_index, 7),
                Down => assert_eq!(connector.to_index, 6),
            }
        }
    }

}
